package com.gotobject.junit.runners;

import com.gotobject.junit.BootstrapWith;
import com.gotobject.junit.If;
import com.gotobject.junit.internal.Bootstrapper;
import com.gotobject.junit.internal.runners.model.EachTestQualifiedNotifier;
import com.gotobject.junit.internal.runners.model.QualifiedNotifier;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

import java.lang.annotation.Annotation;

/**
 * This runner enforces any condition at runtime, defined by a user, imposed on a given test.
 * This is the sweet replacement of the {@literal @Ignore} annotation.
 *
 * A typical usage scenario looks like this:
 *
 * <pre>
 *  &#064;RunWith(ConditionalJUnitClassRunner.class)
 *  public class SampleConditionalTest {
 *
 *      &#064;Test
 *      &#064;If(condition = "isDBConnectionOk")
 *      public void testClientRecord_InGoodState() throws Exception {
 *          // db related stuff
 *      }
 *
 *      public static boolean isDBConnectionOk(){
 *          boolean result = false;
 *          // ... some processing goes here to check if there db connection
 *          // ... then assign it to result.
 *          return result;
 *      }
 * }
 * </pre>
 *
 * Or, if the checking of the condition is located in an external class, a usage scenario like this one:
 * <pre>
 *  &#064;RunWith(ConditionalJUnitClassRunner.class)
 *  public class SampleConditionalTestTwo {
 *
 *      &#064;Test
 *      &#064;If(condition = "isDBConnectionOk", callee = CurrentDBController.class)
 *      public void testClientRecord_InGoodState() throws Exception {
 *          // db related stuff
 *      }
 *
 *      public static class CurrentDBController {
 *           // ... some code goes here.
 *          public boolean isDBConnectionOk(){
 *              boolean result = false;
 *              // ... some processing goes here to check if there db connection
 *              // ... then assign it to result.
 *              return result;
 *          }
 *      }
 * }
 * </pre>
 *
 * @author Huascar A. Sanchez (huascar.sanchez@gmail.com)
 */
public class ConditionalJUnitClassRunner extends BlockJUnit4ClassRunner implements ConditionalRunner {
    private RunNotifier notifier;

    /**
     * Creates a ConditionalJUnitClassRunner to run {@code klass}
     *
     * @param klass containing all the tests to be executed.
	 * @throws InitializationError
     *          if the test class is malformed.
     */
    public ConditionalJUnitClassRunner(Class<?> klass) throws
    InitializationError {
        this(klass, new RunNotifier());
	}

    /**
     * Creates a ConditionalJUnitClassRunner to run {@code klass}
     * @param klass
     *      containing all the tests to be executed.
	 * @param notifier
     *      default run notifier.
     * @throws InitializationError
     *          if the test class is malformed.
     */
    ConditionalJUnitClassRunner(Class<?> klass, RunNotifier notifier) throws
    InitializationError {
        super(klass);
        this.notifier = notifier;
    }

    /**
     *  retrieves a given annotation that will be used by this runner.
     * @param method
     *      containing the annotation we are looking for.
     * @param klass
     *      or annotation class.
     * @return
     *      the annotation we are looking for. null if not found.
     */
    protected static <T extends Annotation> T annotationFor(FrameworkMethod method, Class<? extends T> klass){
		return method.getAnnotation(klass);
	}

    /**
     *  retrieves a given annotation that will be used by this runner.
     * @param klass
     *      containing the annotation we are looking for.
     * @param annotClass
     *      annotation we are looking for.
     * @return
     *      an instance of a given annotation.
     */
    protected static <T extends Annotation> T annotationFor(Class<?> klass, Class<? extends T> annotClass){
		return klass.getAnnotation(annotClass);
	}

    public void bootstrap(Object target) {
        final BootstrapWith annotation 		    = annotationFor(getTestClass().getJavaClass(), BootstrapWith.class);
        final boolean 		isBootstrappingOn   = annotation != null;
        if(isBootstrappingOn) new Bootstrapper(target).boostrap(annotation.value());
    }

    /**
     * the current notifier monitoring the execution of your tests.
     * @return
     *      the {@link RunNotifier} being used to monitor the execution of
	 * 		your tests.
     */
    protected RunNotifier getRunNotifier() {
        if(notifier == null) {
            throw new IllegalStateException("Ouch! you did it again. " +
                    "When are you going to stop doing this? " +
                    "You need a run notifier if you want to execute your tests."
            );
        }
        return notifier;
    }

    public QualifiedNotifier qualify(FrameworkMethod method, RunNotifier notifier) {
        return new EachTestQualifiedNotifier.Builder(
                describeChild(method), notifier)
                .testClass(getTestClass())
                .ifAnnotation(annotationFor(method, If.class))
                .testMethod(method)
        .build();
    }

    @Override
    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
        final QualifiedNotifier shell = qualify(method, notifier);
        shell.fireTestQualification();
        set(notifier);
        if(shell.isIgnored()) return;
        shell.fireQualifiedTestStarted(methodBlock(method));
    }


    /**
     * remeber which notifier is currently monitoring the execution of your tests.
     * @param value 
     *      the {@link RunNotifier} being used to monitor your tests.
     */
    private void set(RunNotifier value) {
       notifier = value;
    }    


    @Override
	protected Statement withBefores(FrameworkMethod method, final Object target, Statement statement) {
		bootstrap(target);
		return super.withBefores(method, target, statement);
	}
}
